/******************************************************************************* * Copyright (c) 2004, 2009 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation * Semion Chichelnitsky (semion@il.ibm.com) - bug 66889 *******************************************************************************/ package org.eclipse.ui.internal.presentations; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import org.eclipse.jface.util.Geometry; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.CTabFolder; import org.eclipse.swt.custom.CTabFolder2Adapter; import org.eclipse.swt.custom.CTabFolderEvent; import org.eclipse.swt.custom.CTabItem; import org.eclipse.swt.custom.ViewForm; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.ControlListener; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; //import org.eclipse.swt.events.MouseAdapter; //import org.eclipse.swt.events.MouseEvent; //import org.eclipse.swt.events.MouseListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.internal.dnd.SwtUtil; import org.eclipse.ui.internal.layout.SizeCache; import org.eclipse.ui.internal.presentations.util.ProxyControl; import org.eclipse.ui.internal.tweaklets.TabBehaviour; import org.eclipse.ui.internal.tweaklets.Tweaklets; import org.eclipse.ui.presentations.IStackPresentationSite; /** * This class implements the tab folders that contains can contain two toolbars * and status text. Wherever possible, the toolbars are aligned with the tabs. * If there is not enough room beside the tabs, the toolbars are aligned with * the status text. This is the same tab folder that is used to arrange views * and editors in Eclipse. * <p> * This is closely related to DefaultPartPresentation, but they have different * responsibilities. This is essentially a CTabFolder that can manage a toolbar. * It should not depend on data structures from the workbench, and its public * interface should only use SWT objects or listeners. DefaultPartPresentation * uses a PaneFolder to arrange views or editors. Knowledge of higher-level data * structures should go there. * </p> * <p> * Although it is not actually a control, the public interface is much like an * SWT control. Implementation-wise, this is actually a combination of a * CTabFolder and a ViewForm. It encapsulates the details of moving the toolbar * between the CTabFolder and the ViewForm, and provides a simpler interface to * the ViewForm/CTabFolder. * </p> * To be consistent with SWT composites, this object can deal with its children * being disposed without warning. This is treated like a removal. * */ public final class PaneFolder { // Tab folder and associated proxy controls private CTabFolder tabFolder; private Control titleAreaProxy; // View form and associated proxy controls private ViewForm viewForm; private ProxyControl contentProxy; private ProxyControl viewFormTopLeftProxy; private ProxyControl viewFormTopRightProxy; private ProxyControl viewFormTopCenterProxy; // Cached sizes of the top-right and top-center controls private SizeCache topRightCache = new SizeCache(); private SizeCache topCenterCache = new SizeCache(); private SizeCache topLeftCache = new SizeCache(); private boolean putTrimOnTop = true; // HACK: Sometimes the topright control isn't resized when // CTabFolder.setBounds is called. // We use the following data structures to detect if this has happened and // force a layout when necessary. private boolean topRightResized = false; private boolean useTopRightOptimization = false; private int lastWidth = 0; // END OF HACK private DisposeListener tabFolderDisposeListener = new DisposeListener() { /* * (non-Javadoc) * * @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent) */ public void widgetDisposed(DisposeEvent e) { PaneFolder.this.widgetDisposed(); } }; /** * Listens for its children being disposed, and removes them if this happens * (although this may indicate a programming error, this behavior is * consistent with SWT composites). */ private DisposeListener prematureDisposeListener = new DisposeListener() { public void widgetDisposed(DisposeEvent e) { Control disposedControl = (Control) e.widget; if (isDisposed()) { return; } // Probably unnecessary, but it can't hurt garbage collection disposedControl.removeDisposeListener(this); if (disposedControl == topLeftCache.getControl()) { setTopLeft(null); } if (disposedControl == topRightCache.getControl()) { setTopRight(null); } if (disposedControl == topCenterCache.getControl()) { setTopCenter(null); } } }; /** * List of PaneFolderButtonListener */ private List buttonListeners = new ArrayList(1); private int state = IStackPresentationSite.STATE_RESTORED; // RAP [rh] unused code // /** // * State of the folder at the last mousedown event. This is used to prevent // * a mouseup over the minimize or maximize buttons from undoing a state // * change that was caused by the mousedown. // */ // private int mousedownState = -1; // RAP [bm]: not used // /** // * Location of the last mousedown event. This is used to determine if the // * mouseup event occured on the same item. // */ // private Point mousedownPoint = new Point(-1, -1); // // /** // * Time of the last mousedown event. This is used to determine if the // * mouseup event occurs within a reasonable time. // */ // private long mousedownTime = 0L; // // /** // * Timeout value for when a pair of mousedown, mouseup events should not be // * interpreted as a click. // */ // private static long CLICK_TIME = 1000; // RAPEND: [bm] // CTabFolder listener private CTabFolder2Adapter expandListener = new CTabFolder2Adapter() { public void minimize(CTabFolderEvent event) { event.doit = false; notifyButtonListeners(IStackPresentationSite.STATE_MINIMIZED); } public void restore(CTabFolderEvent event) { event.doit = false; notifyButtonListeners(IStackPresentationSite.STATE_RESTORED); } public void maximize(CTabFolderEvent event) { event.doit = false; notifyButtonListeners(IStackPresentationSite.STATE_MAXIMIZED); } /* * (non-Javadoc) * * @see org.eclipse.swt.custom.CTabFolder2Adapter#close(org.eclipse.swt.custom.CTabFolderEvent) */ public void close(CTabFolderEvent event) { event.doit = false; notifyCloseListeners((CTabItem) event.item); } public void showList(CTabFolderEvent event) { // RAP [bm]: disabled part list to use the native one // notifyShowListeners(event); // RAPEND: [bm] } }; // RAP [rh] MouseListener useless: no event.time, don't work reliably on CTabFolder // private MouseListener mouseListener = new MouseAdapter() { // public void mouseDown(MouseEvent e) { // mousedownState = getState(); // mousedownPoint = new Point(e.x, e.y); // mousedownTime = e.time & 0x0FFFFFFFFL; // } // // public void mouseUp(MouseEvent e) { // if (e.button == 2 && e.count == 1) { // Point mouseupPoint = new Point(e.x, e.y); // CTabItem item = tabFolder.getItem(mouseupPoint); // long mouseupTime = e.time & 0x0FFFFFFFFL; // if (item != null && (mouseupTime - mousedownTime <= CLICK_TIME)) { // CTabItem mousedownItem = tabFolder.getItem(mousedownPoint); // if (mousedownItem == item) { // notifyCloseListeners(item); // } // } // } // } // // public void mouseDoubleClick(MouseEvent e) { // } // }; private boolean showButtons = true; private boolean minimizeVisible = false; private boolean maximizeVisible = false; /** * Make sure we don't recursively enter the layout() code. */ private boolean inLayout = false; private int tabPosition; /** * Creates a pane folder. This will create exactly one child control in the * given parent. * * @param parent * @param flags */ public PaneFolder(Composite parent, int flags) { // Initialize tab folder { tabFolder = new CTabFolder(parent, flags); tabFolder.setMRUVisible(((TabBehaviour)Tweaklets.get(TabBehaviour.KEY)).enableMRUTabVisibility()); // Create a proxy control to measure the title area of the tab folder // RAP [bm]: SWT.NO_BACKGROUND // titleAreaProxy = new Composite(tabFolder, SWT.NO_BACKGROUND); titleAreaProxy = new Composite(tabFolder, SWT.NONE); // RAPEND: [bm] titleAreaProxy.setVisible(false); titleAreaProxy.addControlListener(new ControlListener() { public void controlMoved(ControlEvent e) { topRightResized = true; } public void controlResized(ControlEvent e) { topRightResized = true; // bug 101683 - we need to do a layout of the PaneFolder // when the title area proxy is resized. if (!inLayout && !PaneFolder.this.isDisposed() && viewForm!=null && contentProxy!=null) { PaneFolder.this.aboutToResize(); // RAP [if] TextSizeUtil workaround - see bug 339334 // PaneFolder.this.layout(false); PaneFolder.this.layout(true); } } }); tabFolder.setTopRight(titleAreaProxy, SWT.FILL); tabFolder.addCTabFolder2Listener(expandListener); // RAP [rh] MouseListener useless: no event.time, don't work reliably on CTabFolder // tabFolder.addMouseListener(mouseListener); tabFolder.addDisposeListener(tabFolderDisposeListener); } // Initialize view form { // RAP [bm]: // viewForm = new ViewForm(tabFolder, SWT.NO_BACKGROUND); viewForm = new ViewForm(tabFolder, SWT.NONE); // RAPEND: [bm] // Only attach these to the viewForm when there's actually a control // to display viewFormTopLeftProxy = new ProxyControl(viewForm); viewFormTopCenterProxy = new ProxyControl(viewForm); viewFormTopRightProxy = new ProxyControl(viewForm); contentProxy = new ProxyControl(viewForm); viewForm.setContent(contentProxy.getControl()); } } /** * Returns the title area (the empty region to the right of the tabs), in * the tab folder's coordinate system. * * @return the title area (the empty region to the right of the tabs) */ public Rectangle getTitleArea() { return titleAreaProxy.getBounds(); } /** * Return the main control for this pane folder * * @return */ public Composite getControl() { return tabFolder; } public void flushTopCenterSize() { topCenterCache.flush(); viewForm.changed(new Control[] {viewFormTopCenterProxy.getControl()}); } public void flushTopLeftSize() { topLeftCache.flush(); viewForm.changed(new Control[] { viewFormTopLeftProxy.getControl() }); } /** * Sets the top-center control (usually a toolbar), or null if none. Note * that the control can have any parent. * * @param topCenter * the top-center control or null if none */ public void setTopCenter(Control topCenter) { if (topCenter == topCenterCache.getControl()) { return; } removeDisposeListener(topCenterCache.getControl()); topCenterCache.setControl(topCenter); if (putTrimOnTop) { viewFormTopCenterProxy.setTarget(null); } else { viewFormTopCenterProxy.setTarget(topCenterCache); } viewForm.changed(new Control[] {viewFormTopCenterProxy.getControl()}); if (topCenter != null) { topCenter.addDisposeListener(prematureDisposeListener); if (!putTrimOnTop) { if (!viewForm.isDisposed()) { viewForm.setTopCenter(viewFormTopCenterProxy.getControl()); } } } else { if (!putTrimOnTop) { if (!viewForm.isDisposed()) { viewForm.setTopCenter(null); } } } } /** * Sets the top-right control (usually a dropdown), or null if none * * @param topRight */ public void setTopRight(Control topRight) { if (topRightCache.getControl() == topRight) { return; } removeDisposeListener(topRightCache.getControl()); topRightCache.setControl(topRight); if (putTrimOnTop) { viewFormTopRightProxy.setTarget(null); } else { viewFormTopRightProxy.setTarget(topRightCache); } if (topRight != null) { topRight.addDisposeListener(prematureDisposeListener); if (!putTrimOnTop) { viewForm.setTopRight(viewFormTopRightProxy.getControl()); } } else { if (!putTrimOnTop) { viewForm.setTopRight(null); } } } /** * Sets the top-left control (usually a title label), or null if none * * @param topLeft */ public void setTopLeft(Control topLeft) { if (topLeftCache.getControl() == topLeft) { return; } removeDisposeListener(topLeftCache.getControl()); topLeftCache.setControl(topLeft); // The top-left control always goes directly in the ViewForm if (topLeft != null) { topLeft.addDisposeListener(prematureDisposeListener); viewFormTopLeftProxy.setTarget(topLeftCache); viewForm.setTopLeft(viewFormTopLeftProxy.getControl()); } else { viewFormTopLeftProxy.setTarget(null); viewForm.setTopLeft(null); } } /** * Optimization: calling this method immediately before setting the * control's bounds will allow for improved caching. */ public void aboutToResize() { useTopRightOptimization = true; topRightResized = false; lastWidth = getControl().getBounds().width; } /** * Cause the folder to hide or show its * Minimize and Maximize affordances. * * @param show * <code>true</code> - the min/max buttons are visible. */ public void showMinMax(boolean show) { showButtons = show; setMaximizeVisible(show); setMinimizeVisible(show); layout(true); } public void layout(boolean flushCache) { if (inLayout) { return; } inLayout = true; try { viewForm.setLayoutDeferred(true); tabFolder.setMinimizeVisible(showButtons && minimizeVisible); tabFolder.setMaximizeVisible(showButtons && maximizeVisible); // Flush the cached sizes if necessary if (flushCache) { topLeftCache.flush(); topRightCache.flush(); topCenterCache.flush(); } // HACK: Force the tab folder to do a layout, since it doesn't always // resize its title area each time setBounds is called. if (!(useTopRightOptimization && (topRightResized || lastWidth == getControl() .getBounds().width))) { // If we can't use the optimization, then we need to force a layout // of the tab folder tabFolder.setTopRight(titleAreaProxy, SWT.FILL); } useTopRightOptimization = false; // END OF HACK // Get the bounds relative to the CTabFolder Rectangle titleArea = titleAreaProxy.getBounds(); Point topRightSize = topRightCache .computeSize(SWT.DEFAULT, SWT.DEFAULT); Point topCenterSize = topCenterCache.computeSize(SWT.DEFAULT, SWT.DEFAULT); // Determine if there is enough room for the trim in the title area int requiredWidth = topRightSize.x + topCenterSize.x; int requiredHeight = Math.max(topRightSize.y, topCenterSize.y); boolean lastTrimOnTop = putTrimOnTop; putTrimOnTop = (titleArea.width >= requiredWidth && titleArea.height >= requiredHeight); Control topRight = topRightCache.getControl(); Control topCenter = topCenterCache.getControl(); if (putTrimOnTop) { // Try to avoid calling setTop* whenever possible, since this will // trigger a layout // of the viewForm. if (!lastTrimOnTop) { // Arrange controls in the title bar viewFormTopCenterProxy.setTarget(null); viewFormTopRightProxy.setTarget(null); viewForm.setTopCenter(null); viewForm.setTopRight(null); } Rectangle topRightArea = new Rectangle(titleArea.x + titleArea.width - topRightSize.x, titleArea.y + (titleArea.height - topRightSize.y) / 2, topRightSize.x, topRightSize.y); if (topRight != null) { // Map the coordinates from the tabFolder back to the control's parent topRight.setBounds(topRight.getDisplay().map(tabFolder, topRight.getParent(), topRightArea)); } if (topCenter != null) { Rectangle topCenterArea = new Rectangle(topRightArea.x - topCenterSize.x, titleArea.y + (titleArea.height - topCenterSize.y) / 2, topCenterSize.x, topCenterSize.y); // Map the coordinates from the tabFolder back to the control's parent topCenter.setBounds(topCenter.getDisplay().map(tabFolder, topCenter.getParent(), topCenterArea)); } } else { if (topCenter != null) { viewFormTopCenterProxy.setTarget(topCenterCache); viewForm.setTopCenter(viewFormTopCenterProxy.getControl()); } if (topRight != null) { viewFormTopRightProxy.setTarget(topRightCache); viewForm.setTopRight(viewFormTopRightProxy.getControl()); } } Rectangle newBounds = tabFolder.getClientArea(); viewForm.setBounds(newBounds); } finally { viewForm.setLayoutDeferred(false); inLayout = false; } viewFormTopRightProxy.layout(); viewFormTopLeftProxy.layout(); viewFormTopCenterProxy.layout(); } public Composite getContentParent() { return viewForm; } public void setContent(Control newContent) { viewForm.setContent(newContent); } /** * Returns the current state of the folder (as shown on the button icons) * * @return one of the IStackPresentationSite.STATE_* constants */ public int getState() { return state; } /** * @param buttonId * one of the IStackPresentationSite.STATE_* constants */ protected void notifyButtonListeners(int buttonId) { // RAP [rh] mousedownState unused, since CTabFolderEvents are used insted of MouseListeners // if (mousedownState == getState()) { Iterator iter = buttonListeners.iterator(); while (iter.hasNext()) { PaneFolderButtonListener listener = (PaneFolderButtonListener) iter .next(); listener.stateButtonPressed(buttonId); } // } } public Control getContent() { return viewForm.getContent(); } /** * Notifies all listeners that the user clicked on the chevron * * @param tabItem */ protected void notifyShowListeners(CTabFolderEvent event) { Iterator iter = buttonListeners.iterator(); while (iter.hasNext()) { PaneFolderButtonListener listener = (PaneFolderButtonListener) iter .next(); listener.showList(event); } } /** * Notifies all listeners that the close button was pressed * * @param tabItem */ protected void notifyCloseListeners(CTabItem tabItem) { Iterator iter = buttonListeners.iterator(); while (iter.hasNext()) { PaneFolderButtonListener listener = (PaneFolderButtonListener) iter .next(); listener.closeButtonPressed(tabItem); } } /** * Sets the state that will be shown on the CTabFolder's buttons * * @param state * one of the IStackPresentationSite.STATE_* constants */ public void setState(int state) { this.state = state; tabFolder.setMinimized(state == IStackPresentationSite.STATE_MINIMIZED); tabFolder.setMaximized(state == IStackPresentationSite.STATE_MAXIMIZED); } public void addButtonListener(PaneFolderButtonListener listener) { buttonListeners.add(listener); } public void removeButtonListener(PaneFolderButtonListener listener) { buttonListeners.remove(listener); } public void setTabPosition(int newTabPosition) { tabPosition = newTabPosition; tabFolder.setTabPosition(tabPosition); } public int getTabPosition() { return tabPosition; } public boolean isDisposed() { return tabFolder == null || tabFolder.isDisposed(); } public CTabItem createItem(int style, int index) { return new CTabItem(tabFolder, style, index); } public Point computeMinimumSize() { Point result = Geometry.getSize(tabFolder.computeTrim(0, 0, 0, 0)); // Add some space for the minimize and maximize buttons plus a tab. // Right now this isn't exposed from SWT as API, so we just add 50 // pixels. result.x += 100; return result; } /** * Removes the dispose listener from the given control, unless the given * control is null or disposed. * * @param oldControl * control to detach the dispose listener from */ private void removeDisposeListener(Control oldControl) { if (!SwtUtil.isDisposed(oldControl)) { oldControl.removeDisposeListener(prematureDisposeListener); } } private void widgetDisposed() { removeDisposeListener(topCenterCache.getControl()); topCenterCache.setControl(null); removeDisposeListener(topRightCache.getControl()); topRightCache.setControl(null); removeDisposeListener(topLeftCache.getControl()); topLeftCache.setControl(null); } public Point getChevronLocation() { // get the last visible item int numItems = tabFolder.getItemCount(); CTabItem item = null, tempItem = null; for (int i = 0; i < numItems; i++) { tempItem = tabFolder.getItem(i); if (tempItem.isShowing()) { item = tempItem; } } // if we have no visible tabs, abort. if (item == null) { return new Point(0, 0); } Rectangle itemBounds = item.getBounds(); int x = itemBounds.x + itemBounds.width; int y = itemBounds.y + itemBounds.height; return new Point(x, y); } /////////////////////////////////////////////////////////////////////////////////////// // The remainder of the methods in this class redirect directly to // CTabFolder methods public void setSelection(int selection) { tabFolder.setSelection(selection); } public void showItem(int selection) { tabFolder.showItem(tabFolder.getItem(selection)); } /** * @param i * @param j * @param k * @param l * @return */ public Rectangle computeTrim(int i, int j, int k, int l) { return tabFolder.computeTrim(i, j, k, l); } /** * @param b */ public void setUnselectedCloseVisible(boolean b) { tabFolder.setUnselectedCloseVisible(b); } /** * @param fgColor */ public void setSelectionForeground(Color fgColor) { tabFolder.setSelectionForeground(fgColor); } /** * Set the selection gradient with the activation state. * @param bgColors * @param percentages * @param vertical */ public void setSelectionBackground(Color[] bgColors, int[] percentages, boolean vertical) { tabFolder.setSelectionBackground(bgColors, percentages, vertical); } public CTabItem getItem(int idx) { return tabFolder.getItem(idx); } public int getSelectionIndex() { return tabFolder.getSelectionIndex(); } public int getTabHeight() { return tabFolder.getTabHeight(); } public int indexOf(CTabItem toFind) { return tabFolder.indexOf(toFind); } public void setTabHeight(int height) { tabFolder.setTabHeight(height); } /** * @return */ public int getItemCount() { return tabFolder.getItemCount(); } /** * @return */ public CTabItem[] getItems() { return tabFolder.getItems(); } public CTabItem getItem(Point toGet) { return tabFolder.getItem(toGet); } public CTabItem getSelection() { return tabFolder.getSelection(); } /** * @param isVisible */ public void setMinimizeVisible(boolean isVisible) { tabFolder.setMinimizeVisible(isVisible); minimizeVisible = isVisible; } /** * Changes the minimum number of characters to display in a pane folder tab. * This control how much information will be displayed to the user. * * @param count * The number of characters to display in the tab folder; this * value should be a positive integer. * @see org.eclipse.swt.custom.CTabFolder#setMinimumCharacters(int) */ public void setMinimumCharacters(int count) { tabFolder.setMinimumCharacters(count); } /** * @param isVisible */ public void setMaximizeVisible(boolean isVisible) { tabFolder.setMaximizeVisible(isVisible); maximizeVisible = isVisible; } /** * @param traditionalTab */ public void setSimpleTab(boolean traditionalTab) { // RAP [bm]: // tabFolder.setSimple(traditionalTab); // RAPEND: [bm] } /** * @param b */ public void setUnselectedImageVisible(boolean b) { tabFolder.setUnselectedImageVisible(b); } /** * @param b */ public void setSingleTab(boolean b) { tabFolder.setSingle(b); } public void hideTitle() { tabFolder.setTabHeight(0); } public ViewForm getViewForm() { return viewForm; } /** * Propogate the visibility change requests to the proxy controls. When * their target is null, they no longer get visibility updates. Currently * this only propagates the changes to the ProxyControls held by this * folder. * * @param visible * <code>true</code> - it's visible. */ public void setVisible(boolean visible) { contentProxy.setVisible(visible); viewFormTopCenterProxy.setVisible(visible); viewFormTopLeftProxy.setVisible(visible); viewFormTopRightProxy.setVisible(visible); } }